package solver;

import gurobi.GRB;
import gurobi.GRBEnv;
import gurobi.GRBException;
import gurobi.GRBLinExpr;
import gurobi.GRBModel;
import gurobi.GRBVar;

import java.util.HashMap;
import java.util.List;

import model.BalanceObjective;
import model.Location;
import model.Machine;
import model.Neighborhood;
import model.Problem;
import model.Process;
import model.Resource;
import model.Service;
import model.Solution;

public class GrbLeanIPModelBuilder {
	final int P, M, R, L, S, N, BO;

	Problem problem;
	Solution initial_solution;
	boolean verbose = true;

	GRBEnv env;
	GRBModel model = null;

	// ######## START OF VARIABLES#############
	HashMap<Integer, GRBVar>[] x;

	GRBVar[][] y; //

	GRBVar[][] k;
	GRBVar[][] lo_cost; // load cost

	GRBVar[][][] a;
	GRBVar[][] abs;
	GRBVar[][] ba_cost; // balance cost

	GRBVar pm_cost; // process move cost

	GRBVar[] se; // service move
	GRBVar[] abs_se;
	GRBVar[] se_cost;

	GRBVar mm_cost; // machine move cost

	// ######## END OF VARIABLES#############

	public GrbLeanIPModelBuilder(Problem problem, boolean verbose) {
		this.problem = problem;
		this.verbose = verbose;
		P = problem.getProcesses().size();
		M = problem.getMachines().size();
		R = problem.getResources().size();
		L = problem.getLocations().size();
		S = problem.getServices().size();
		N = problem.getNeighborhoods().size();
		BO = problem.getBalanceObjectives().size();

		try {
			env = new GRBEnv("grb_build_lean_model.log");
			env.set(GRB.DoubleParam.TimeLimit, 600);
			env.set(GRB.IntParam.Method, 1); // dual simplex
			env.set(GRB.IntParam.Threads, 2);
			model = new GRBModel(env);
		} catch (GRBException e) {
			System.out.println("GRB Error code: " + e.getErrorCode() + " -> "
					+ e.getMessage());
		}
	}

	public void setSolution(Solution solution) {
		this.initial_solution = solution;
	}

	public boolean solve() {
		boolean flag = false;
		// assignEligibleMachinesToProcesses();
		try {
			addVariables();

			addConstraints0();
			addConstraints1();
			addConstraints2();
			addConstraints3();
			addConstraints4();
			addConstraints5();

			addSoftConstraints0();
			addSoftConstraints1();
			addSoftConstraints2();
			addSoftConstraints3();
			addSoftConstraints4();

			model.update();
			model.write("machine_reassignment_lean.lp");
			model.optimize();
			model.terminate();
			// if (model.get(GRB.IntAttr.Status) == GRB.Status.OPTIMAL)
			// System.out.println("OPTIMAL SOLUTION FOUND :)");
			if (model.get(GRB.IntAttr.SolCount) > 0) {
				flag = true;
				System.out.printf("Objective value=%.0f\n",
						model.get(GRB.DoubleAttr.ObjVal));
			} else {
				System.out.println("Objective value= DNF");
			}
		} catch (GRBException e) {
			e.printStackTrace();
		}
		return flag;
	}

	@SuppressWarnings("unchecked")
	private void addVariables() throws GRBException {
		x = new HashMap[P];
		for (int p = 0; p < P; p++) {
			x[p] = new HashMap<Integer, GRBVar>();
			Process proc = problem.getProcesses().get(p);
			for (Integer machine_id : proc.getEligibleMachines()) {
				x[p].put(
						machine_id,
						model.addVar(0.0, 1.0, 0.0, GRB.BINARY,
								String.format("x%d_%d", p, machine_id)));
			}
		}

		y = new GRBVar[S][L];
		for (int s = 0; s < S; s++)
			for (int l = 0; l < L; l++) {
				y[s][l] = model.addVar(0.0, 1.0, 0.0, GRB.BINARY,
						String.format("y%d_%d", s, l));
			}

		k = new GRBVar[M][R];
		lo_cost = new GRBVar[M][R];
		for (int m = 0; m < M; m++) {
			for (int r = 0; r < R; r++) {
				Resource resource = problem.getResources().get(r);
				k[m][r] = model.addVar(0.0, Double.POSITIVE_INFINITY, 0.0,
						GRB.INTEGER, String.format("k%d_%d", m, r));
				lo_cost[m][r] = model.addVar(0.0, Double.POSITIVE_INFINITY,
						resource.getWeightLoadCost(), GRB.INTEGER,
						String.format("lc%d_%d", m, r));

			}
		}

		a = new GRBVar[BO][M][2];
		abs = new GRBVar[BO][M];
		ba_cost = new GRBVar[BO][M];
		for (int bo_index = 0; bo_index < BO; bo_index++) {
			BalanceObjective bo = problem.getBalanceObjectives().get(bo_index);
			for (int m = 0; m < M; m++) {
				abs[bo_index][m] = model.addVar(0.0, Double.POSITIVE_INFINITY,
						0.0, GRB.CONTINUOUS,
						String.format("abs_bo%d_m%d", bo_index, m));
				ba_cost[bo_index][m] = model.addVar(0.0,
						Double.POSITIVE_INFINITY, bo.weightBalanceCost,
						GRB.CONTINUOUS,
						String.format("ba_cost_bo%d_m%d", bo_index, m));
				a[bo_index][m][0] = model.addVar(0.0, Double.POSITIVE_INFINITY,
						0.0, GRB.INTEGER, String.format("a_bo%d_m%d_r%d",
								bo_index, m, bo.resourcea));
				a[bo_index][m][1] = model.addVar(0.0, Double.POSITIVE_INFINITY,
						0.0, GRB.INTEGER, String.format("a_bo%d_m%d_r%d",
								bo_index, m, bo.resourceb));
			}
		}

		pm_cost = model.addVar(0.0, Double.POSITIVE_INFINITY,
				problem.getWeightProcessMoveCost(), GRB.CONTINUOUS, "pm_cost");

		se = new GRBVar[S];
		abs_se = new GRBVar[S - 1];
		se_cost = new GRBVar[S - 1];
		for (int s = 0; s < S; s++) {
			se[s] = model.addVar(0.0, Double.POSITIVE_INFINITY, 0.0,
					GRB.INTEGER, String.format("se_s%d", s));
		}
		for (int s = 0; s < S - 1; s++) {
			abs_se[s] = model.addVar(0.0, Double.POSITIVE_INFINITY, 0.0,
					GRB.INTEGER, String.format("abs_se_s%d", s));
			if (s == S - 2)
				se_cost[s] = model.addVar(0.0, Double.POSITIVE_INFINITY,
						problem.getWeightServiceMoveCost(), GRB.INTEGER,
						String.format("se_cost_s%d", s));
			else
				se_cost[s] = model.addVar(0.0, Double.POSITIVE_INFINITY, 0.0,
						GRB.INTEGER, String.format("se_cost_s%d", s));
		}

		mm_cost = model.addVar(0.0, Double.POSITIVE_INFINITY,
				problem.getWeightMachineMoveCost(), GRB.INTEGER, "mm_cost");

		model.update();
	}

	private void addConstraints0() throws GRBException {
		for (int p = 0; p < P; p++) {
			GRBLinExpr expr = new GRBLinExpr();
			Process aProcess = problem.getProcesses().get(p);
			for (Integer machine_id : aProcess.getEligibleMachines()) {
				expr.addTerm(1.0, getxVar(p, machine_id));
			}
			model.addConstr(expr, GRB.EQUAL, 1.0,
					String.format("assignment_constraint_p%d", p));
		}
	}

	// capacity constraints
	private void addConstraints1() throws GRBException {
		for (int machine_id = 0; machine_id < M; machine_id++) {
			for (int resource_id = 0; resource_id < R; resource_id++) {
				GRBLinExpr expr = new GRBLinExpr();
				for (int process_id = 0; process_id < P; process_id++) {
					Process process = problem.getProcesses().get(process_id);
					if (process.getEligibleMachines().contains(
							new Integer(machine_id)))
						expr.addTerm(problem.R[process_id][resource_id],
								getxVar(process_id, machine_id));
				}
				int capacity = problem.C[machine_id][resource_id];
				model.addConstr(expr, GRB.LESS_EQUAL, capacity, String.format(
						"capacity_constr_m%d_r%d", machine_id, resource_id));
			}
		}
	}

	// conflict constraints
	private void addConstraints2() throws GRBException {
		int k = 0;
		for (Service service : problem.getServices()) {
			List<Integer> processes = service.getProcesses();
			if (processes.size() == 1)
				continue;
			else {
				for (int machine_id = 0; machine_id < M; machine_id++) {
					GRBLinExpr expr = new GRBLinExpr();
					boolean termsExist = false;
					for (Integer process_id : processes) {
						Process process = problem.getProcesses()
								.get(process_id);
						if (process.getEligibleMachines().contains(
								new Integer(machine_id))) {
							expr.addTerm(1.0, getxVar(process_id, machine_id));
							termsExist = true;
						}
					}
					if (termsExist) {
						model.addConstr(expr, GRB.LESS_EQUAL, 1.0,
								String.format("conflict_constr_s%d", k));
						k++;
					}
				}
			}
		}
	}

	// spread constraints
	private void addConstraints3() throws GRBException {
		int k = 0;
		for (Service service : problem.getServices()) {
			if (service.getSpreadMin() == 0) {
				k++;
				continue;
			}
			GRBLinExpr expr = new GRBLinExpr();
			for (Location location : problem.getLocations()) {
				GRBLinExpr expr1 = new GRBLinExpr();
				GRBLinExpr expr2 = new GRBLinExpr();
				for (Integer m : location.getMachines()) {
					for (Integer p : service.getProcesses()) {
						GRBVar xVar = getxVar(p, m);
						if (xVar != null) {
							expr1.addTerm(1.0, xVar);
							expr2.addTerm(1.0, xVar);
						}
					}
				}
				expr1.addTerm(-1.0, y[k][location.getId()]);
				expr2.addTerm(-location.getMachines().size(),
						y[k][location.getId()]);
				model.addConstr(
						expr1,
						GRB.GREATER_EQUAL,
						0.0,
						String.format("spread_constr_s%d_l%d_a", k,
								location.getId()));
				model.addConstr(
						expr2,
						GRB.LESS_EQUAL,
						0.0,
						String.format("spread_constr_s%d_l%d_b", k,
								location.getId()));
				expr.addTerm(1.0, y[k][location.getId()]);
			}
			model.addConstr(expr, GRB.GREATER_EQUAL, service.getSpreadMin(),
					String.format("spread_constr_s%d", k));
			k++;
		}
	}

	// Dependency constraints
	private void addConstraints4() throws GRBException {
		int sa_index = 0;
		for (Service servicea : problem.getServices()) {
			if (servicea.getDependsOn().isEmpty()) {
				sa_index++;
				continue;
			}
			for (Integer sb_index : servicea.getDependsOn()) {
				Service serviceb = problem.getServices().get(sb_index);
				for (Integer pa_index : servicea.getProcesses()) {
					for (Neighborhood neighborhood : problem.getNeighborhoods()) {
						GRBLinExpr expr = new GRBLinExpr();
						for (Integer m_nb : neighborhood.getMachines()) {
							GRBVar xVar_pa_nb = getxVar(pa_index, m_nb);
							if (xVar_pa_nb != null) {
								expr.addTerm(1.0, xVar_pa_nb);
							}
							for (Integer pb_index : serviceb.getProcesses()) {
								GRBVar xVar_pb_nb = getxVar(pb_index, m_nb);
								if (xVar_pb_nb != null) {
									expr.addTerm(-1.0, xVar_pb_nb);
								}
							}
						}
						model.addConstr(expr, GRB.LESS_EQUAL, 0.0, String
								.format("depends_constr_sa%d_sb%d_p%d_n%d",
										sa_index, sb_index, pa_index,
										neighborhood.getId()));
					}
				}
			}
			sa_index++;
		}
	}

	// Transient constraints
	private void addConstraints5() throws GRBException {
		for (int r = 0; r < R; r++) {
			Resource resource = problem.getResourceByIndex(r);
			if (!resource.isTransientUsage())
				continue;
			for (Integer m : initial_solution.getMachinesInSolution()) {
				GRBLinExpr expr = new GRBLinExpr();
				int capacity = problem.C[m][r];
				for (int p = 0; p < P; p++) {
					if (initial_solution.getMachineIdForProcessId(p) == m) {
						// process already exists in solution in that machine
						capacity -= problem.R[p][r];
					} else {
						GRBVar xVar = getxVar(p, m);
						if (xVar != null)
							expr.addTerm(problem.R[p][r], xVar);
					}
				}

				model.addConstr(expr, GRB.LESS_EQUAL, capacity,
						String.format("transient_constr_m%d_r%d", m, r));
			}
		}
	}

	// load cost
	private void addSoftConstraints0() throws GRBException {
		for (int r = 0; r < R; r++) {
			for (int m = 0; m < M; m++) {
				GRBLinExpr expr1 = new GRBLinExpr();
				GRBLinExpr expr2 = new GRBLinExpr();
				GRBLinExpr expr3 = new GRBLinExpr();
				for (int p = 0; p < P; p++) {
					GRBVar xVar = getxVar(p, m);
					if (xVar != null) {
						expr1.addTerm(problem.R[p][r] / 2.0, xVar);
						expr2.addTerm(-problem.R[p][r], xVar);
						expr3.addTerm(problem.R[p][r], xVar);
					}
				}
				expr1.addTerm(0.5, k[m][r]);
				expr1.addTerm(-1.0, lo_cost[m][r]);
				int safety_capacity = problem.getMachines().get(m)
						.getSafetyCapacityFor("r" + r);
				model.addConstr(expr1, GRB.EQUAL, safety_capacity,
						String.format("load_cost_constr_m%d_r%d_a", m, r));

				expr2.addTerm(1.0, k[m][r]);
				model.addConstr(expr2, GRB.GREATER_EQUAL, 0.0,
						String.format("load_cost_constr_m%d_r%d_b", m, r));

				expr3.addTerm(1.0, k[m][r]);
				model.addConstr(expr3, GRB.GREATER_EQUAL, 0.0,
						String.format("load_cost_constr_m%d_r%d_c", m, r));
			}
		}
	}

	// balance cost
	private void addSoftConstraints1() throws GRBException {
		int bo_index = 0;
		for (BalanceObjective bo : problem.getBalanceObjectives()) {
			for (int m = 0; m < M; m++) {
				GRBLinExpr expr1a = new GRBLinExpr();
				GRBLinExpr expr1b = new GRBLinExpr();
				for (int p = 0; p < P; p++) {
					GRBVar xVar = getxVar(p, m);
					if (xVar != null) {
						expr1a.addTerm(problem.R[p][bo.resourcea], xVar);
						expr1b.addTerm(problem.R[p][bo.resourceb], xVar);
					}
				}
				expr1a.addTerm(1.0, a[bo_index][m][0]);
				expr1b.addTerm(1.0, a[bo_index][m][1]);

				int capacity_a = problem.getMachines().get(m)
						.getCapacityFor("r" + bo.resourcea);
				int capacity_b = problem.getMachines().get(m)
						.getCapacityFor("r" + bo.resourceb);
				model.addConstr(expr1a, GRB.EQUAL, capacity_a, String.format(
						"balance_cost_constr_bo%d_m%d_r%d_a%d_%d", bo_index, m,
						bo.resourcea, m, bo.resourcea));
				model.addConstr(expr1b, GRB.EQUAL, capacity_b, String.format(
						"balance_cost_constr_bo%d_m%d_r%d_a%d_%d", bo_index, m,
						bo.resourceb, m, bo.resourceb));
			}
			for (int m = 0; m < M; m++) {
				GRBLinExpr expr2 = new GRBLinExpr();
				GRBLinExpr expr3 = new GRBLinExpr();
				GRBLinExpr expr4 = new GRBLinExpr();
				expr2.addTerm(bo.target / 2.0, a[bo_index][m][0]);
				expr2.addTerm(-0.5, a[bo_index][m][1]);
				expr2.addTerm(0.5, abs[bo_index][m]);
				expr2.addTerm(-1.0, ba_cost[bo_index][m]);
				model.addConstr(expr2, GRB.EQUAL, 0.0, String.format(
						"balance_cost_constr_bo%d_m%d_a", bo_index, m));

				expr3.addTerm(1.0, abs[bo_index][m]);
				expr3.addTerm(bo.target, a[bo_index][m][0]);
				expr3.addTerm(-1.0, a[bo_index][m][1]);
				model.addConstr(expr3, GRB.GREATER_EQUAL, 0.0, String.format(
						"balance_cost_constr_bo%d_m%d_b", bo_index, m));

				expr4.addTerm(1.0, abs[bo_index][m]);
				expr4.addTerm(-bo.target, a[bo_index][m][0]);
				expr4.addTerm(1.0, a[bo_index][m][1]);
				model.addConstr(expr4, GRB.GREATER_EQUAL, 0.0, String.format(
						"balance_cost_constr_bo%d_m%d_c", bo_index, m));
			}
			bo_index++;
		}
	}

	// process move cost
	private void addSoftConstraints2() throws GRBException {
		GRBLinExpr expr = new GRBLinExpr();
		int sum = 0;
		for (int p = 0; p < P; p++) {
			int pmc = problem.getProcesses().get(p).getProcessMoveCost();
			for (int m = 0; m < M; m++) {
				GRBVar xVar = getxVar(p, m);
				if (xVar == null)
					continue;
				if (initial_solution.getMachineIdForProcessId(p) == m) {
					sum += pmc;
					expr.addTerm(-pmc, xVar);
				} else {
					expr.addTerm(pmc, xVar);
				}
			}
		}
		expr.addTerm(-2.0, pm_cost);
		model.addConstr(expr, GRB.EQUAL, -sum, "process_move_cost");
	}

	// service move cost
	private void addSoftConstraints3() throws GRBException {
		if (S <= 1)
			return;
		for (int s = 0; s < S; s++) {
			GRBLinExpr expr = new GRBLinExpr();
			Service service = problem.getServices().get(s);
			for (Integer p : service.getProcesses()) {
				for (int m = 0; m < M; m++) {
					GRBVar xVar = getxVar(p, m);
					if (xVar == null)
						continue;
					if (initial_solution.getMachineIdForProcessId(p) == m) {
						expr.addTerm(-1.0, xVar);
					} else {
						expr.addTerm(1.0, xVar);
					}
				}
			}
			expr.addTerm(-2.0, se[s]);
			model.addConstr(expr, GRB.EQUAL, -service.getProcesses().size(),
					String.format("service_move_cost_constr_s%d", s));
		}

		GRBLinExpr expr1 = new GRBLinExpr();
		expr1.addTerm(0.5, se[0]);
		expr1.addTerm(0.5, se[1]);
		expr1.addTerm(0.5, abs_se[0]);
		expr1.addTerm(-1.0, se_cost[0]);
		model.addConstr(expr1, GRB.EQUAL, 0,
				String.format("service_move_cost_constr_max_s0_a"));

		GRBLinExpr expr2 = new GRBLinExpr();
		expr2.addTerm(1.0, abs_se[0]);
		expr2.addTerm(1.0, se[0]);
		expr2.addTerm(-1.0, se[1]);
		model.addConstr(expr2, GRB.GREATER_EQUAL, 0,
				String.format("service_move_cost_constr_max_s0_b"));

		GRBLinExpr expr3 = new GRBLinExpr();
		expr3.addTerm(1.0, abs_se[0]);
		expr3.addTerm(-1.0, se[0]);
		expr3.addTerm(1.0, se[1]);
		model.addConstr(expr3, GRB.GREATER_EQUAL, 0,
				String.format("service_move_cost_constr_max_s0_c"));

		for (int s = 2; s < S; s++) {
			GRBLinExpr expr4 = new GRBLinExpr();
			expr4.addTerm(0.5, se_cost[s - 2]);
			expr4.addTerm(0.5, se[s]);
			expr4.addTerm(0.5, abs_se[s - 1]);
			expr4.addTerm(-1.0, se_cost[s - 1]);
			model.addConstr(expr4, GRB.EQUAL, 0,
					String.format("service_move_cost_constr_max_s%d_a", s));

			GRBLinExpr expr5 = new GRBLinExpr();
			expr5.addTerm(1.0, abs_se[s - 1]);
			expr5.addTerm(1.0, se_cost[s - 2]);
			expr5.addTerm(-1.0, se[s]);
			model.addConstr(expr5, GRB.GREATER_EQUAL, 0,
					String.format("service_move_cost_constr_max_s%d_b", s));

			GRBLinExpr expr6 = new GRBLinExpr();
			expr6.addTerm(1.0, abs_se[s - 1]);
			expr6.addTerm(-1.0, se_cost[s - 2]);
			expr6.addTerm(1.0, se[s]);
			model.addConstr(expr6, GRB.GREATER_EQUAL, 0,
					String.format("service_move_cost_constr_max_s%d_c", s));
		}
	}

	// machine move cost
	private void addSoftConstraints4() throws GRBException {
		GRBLinExpr expr = new GRBLinExpr();
		for (int p = 0; p < P; p++) {
			int msource = initial_solution.getMachineIdForProcessId(p);
			Machine aMachine = problem.getMachines().get(msource);
			for (int mdest = 0; mdest < M; mdest++) {
				if (msource != mdest) {
					GRBVar xVar = getxVar(p, mdest);
					if (xVar == null)
						continue;
					expr.addTerm(aMachine.getMachineMoveCost(mdest), xVar);
				}
			}
		}
		expr.addTerm(-1.0, mm_cost);
		model.addConstr(expr, GRB.EQUAL, 0, "machine_move_cost");
	}

	private GRBVar getxVar(int process_id, Integer machine_id) {
		return x[process_id].get(machine_id);
	}

	public Solution saveToSolution() {
		Solution solution = new Solution(problem);
		for (int p = 0; p < P; p++) {
			Process aProcess = problem.getProcesses().get(p);
			for (Integer m : aProcess.getEligibleMachines()) {
				try {
					double x_value = getxVar(p, m).get(GRB.DoubleAttr.X);
					if (x_value > 0.9999) {
						solution.assignProcessToMachine(p, m);
					}
				} catch (GRBException e) {
					e.printStackTrace();
				}
			}
		}
		return solution;
	}
}
